import random
import numpy as np
import pandas as pd


class Data_loader():
    """
    A data loader:
    load X & Y form program variables or file.
    """

    def __init__(self):
        return

    def load_dataset_inline(self, X, Y) -> tuple:
        """
        load dataset from program variable
        """
        return X, Y

    def load_dataset_from_file(self, path) -> tuple:
        """
        load dataset from csv
        """
        df = pd.read_csv(path)
        return np.array(df.iloc[:, :-1]), np.array(df.iloc[:, -1])


class Data_generator():
    """
    A Data Generator
    Generate X & Y (linear combinations of X)
    X is (self.length * self._X_num) matrix
    Y is (self.length * 1) vector
    After initialization, linear data with Gaussian
    noise can be output to a file or returned as variables in a program.
    """

    def __init__(self, mu = 0, gamma = 0.05):
        """init"""
        self._X_num = None
        self._params = None
        self.mu = mu
        self.gamma = gamma
        self.length = 30

    def random_initialize(self) -> list:
        """
        initialize the number of feature randomly
        the number of parameters is larger than that of X
        return the parameters [theta1, theta2$...,theta n,Bias]
        return : list
        """
        # Init randomly
        self._X_num = random.randint(1, 3)
        self._params = [round(random.random() * 5, 2) for _ in range(self._X_num + 1)]
        return self._params

    def set_param_initialize(self, X_num, params, length) -> None:
        """
        set the parameter manually. Besides, the method would check to see if the
        length of parameter and the number of X match.
        If not, we raise error.
        return : None
        """
        # Type checking
        if not (isinstance(X_num, int) or isinstance(params, list) or isinstance(length, int)):
            raise TypeError("Check input Type Again!")
        if not len(params) == X_num + 1:
            raise IndexError('X_num and the length of param do not match!')
        if length <= 0:
            raise IndexError('length must lager than ZERO!')
        # Init
        self._X_num = X_num
        self._params = params
        self.length = length

    def generate(self, path_or_buffer=None) -> {None, tuple}:
        """
        Generate dataset with Gauss noise
        if we want to output to a file, this method return Nothing.
        Otherwise, it returns the X, Y
        """
        # Type checking
        if self._X_num is None or self._params is None:
            raise TypeError("please initialize parameters first!")

        Y = np.zeros(self.length)
        X = []

        # Linear Combination
        for ind in range(self._X_num):
            X_temp = np.linspace(0, 10, self.length, endpoint=True)
            Y += self._params[ind] * X_temp
            X.append(X_temp)

        # Add Bias
        Y += self._params[-1]

        # Add gauss noise
        for Xi in X:
            Xi += random.gauss(self.mu, self.gamma)
        Y += random.gauss(self.mu, self.gamma)

        # Convert list to np.array & match dimension
        X = np.array(X).T
        Y = Y.reshape(Y.size, 1)

        # Output
        if path_or_buffer:
            Z = pd.DataFrame(np.append(X, Y, axis=1))
            Z.to_csv(path_or_buf=path_or_buffer, encoding='utf8', index=False)
            return
        return X, Y
